package anaglyphe;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GraphicsConfigTemplate;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusEvent;
import java.awt.event.InputMethodEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import javax.media.j3d.Canvas3D;
import javax.media.j3d.GraphicsConfigTemplate3D;
import javax.media.j3d.ImageComponent2D;
import javax.media.j3d.RestrictedAccessException;
import javax.media.j3d.Screen3D;
import javax.media.j3d.View;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;

import com.sun.j3d.exp.swing.impl.AutoOffScreenCanvas3D;

public class Canvas3DAnaglyph extends JPanel implements AncestorListener {
	private static final long serialVersionUID = 1L;

	private static double METRES_PAR_PIXEL = 0.0254 / 90.0;
	private GraphicsConfiguration configurationGraphique;

	private InternalCanvas3D canevasGauche;
	private InternalCanvas3D canevasDroite;

	private BufferedImage imageEcran = null;
	private volatile int largeurImage = 0;
	private volatile int hauteurImage = 0;

	private long changementTempsDernier = 0;
	private long changementTempsCourant = 0;
	private boolean estOeilGaucheInverse = false;
	private boolean estOeilDroitInverse = false;

	private int[] tamponImage = null;
	private int[] tamponGauche = null;
	private int[] tamponDroite = null;

	private ReentrantLock accesImage = null;
	private Condition conditionCopieImage = null;
	private Condition conditionTailleImage = null;
	private boolean estImageCopiee = false;
	private boolean estRendu = false;

	private Runnable peintre = null;

	private static final int LIMITE_ATTENTE = 200000000;
	private static final int LIMITE_CHANGEMENT = 400000000;

	public Canvas3DAnaglyph() {
		this(null, null);
	}

	public Canvas3DAnaglyph(GraphicsDevice moyen) {
		this(null, moyen);
	}

	public Canvas3DAnaglyph(GraphicsConfigTemplate3D modele) {
		this(modele, null);
	}

	public Canvas3DAnaglyph(GraphicsConfigTemplate3D modele,
			GraphicsDevice moyen) {
		if (modele == null) {
			modele = new GraphicsConfigTemplate3D();
		}
		modele.setStereo(GraphicsConfigTemplate.UNNECESSARY);
		modele.setDoubleBuffer(GraphicsConfigTemplate.UNNECESSARY);

		if (moyen == null) {
			configurationGraphique = GraphicsEnvironment
					.getLocalGraphicsEnvironment().getDefaultScreenDevice()
					.getBestConfiguration(modele);
		} else {
			configurationGraphique = moyen.getBestConfiguration(modele);
		}

		addAncestorListener(this);
		setDoubleBuffered(false);

		setFocusable(true);

		accesImage = new ReentrantLock();
		conditionCopieImage = accesImage.newCondition();
		conditionTailleImage = accesImage.newCondition();

		peintre = new Repainter();
	}

	private class Repainter implements Runnable {
		public void run() {
			try {
				accesImage.lock();
				int argb = 0;

				int gLeft = 0;
				int bLeft = 0;

				int gRight = 0;
				int bRight = 0;

				int aPaint = 0;
				int rPaint = 0;
				int gPaint = 0;
				int bPaint = 0;

				int i = 0;

				for (int j = 0; j < hauteurImage; j++) {
					for (int k = 0; k < largeurImage; k++) {

						argb = tamponGauche[i];

						gLeft = (argb >> 8) & 0xFF;
						bLeft = argb & 0xFF;

						argb = tamponDroite[i];

						gRight = (argb >> 8) & 0xFF;
						bRight = argb & 0xFF;

						aPaint = (argb >> 24) & 0xFF;
						rPaint = (int) (0.7 * gLeft + 0.3 * bLeft);
						gPaint = gRight;
						bPaint = bRight;

						tamponImage[i++] = (aPaint << 24) | (rPaint << 16)
								| (gPaint << 8) | bPaint;
					}
				}
				estImageCopiee = true;
				conditionCopieImage.signal();
			} finally {
				accesImage.unlock();
			}
			Canvas3DAnaglyph.this.repaint();
		}
	}

	private void apresEchange(int eye) {
		changementTempsCourant = System.nanoTime();
		if ((changementTempsCourant - changementTempsDernier) > LIMITE_CHANGEMENT) {
			estOeilGaucheInverse = false;
			estOeilDroitInverse = false;
		}
		changementTempsDernier = changementTempsCourant;

		if (eye == View.LEFT_EYE_VIEW) {
			if (estOeilGaucheInverse) {
				return;
			}
			estOeilGaucheInverse = true;
		}

		if (eye == View.RIGHT_EYE_VIEW) {
			if (estOeilDroitInverse) {
				return;
			}
			estOeilDroitInverse = true;
		}

		if (!estOeilGaucheInverse || !estOeilDroitInverse)
			return;

		if (!accesImage.tryLock()) {
			estOeilGaucheInverse = false;
			estOeilDroitInverse = false;
			return;
		}

		try {
			estImageCopiee = false;

			SwingUtilities.invokeLater(peintre);

			while (!estImageCopiee) {
				estRendu = true;

				try {
					conditionCopieImage.awaitNanos(LIMITE_ATTENTE);
					estImageCopiee = true;
				} catch (InterruptedException e) {
				}
			}

			estRendu = false;
			conditionTailleImage.signal();
		} finally {
			estOeilGaucheInverse = false;
			estOeilDroitInverse = false;

			accesImage.unlock();
		}
	}

	public void ancestorAdded(AncestorEvent event) {
		Dimension sz = getSize();

		if (sz.width == 0) {
			sz.width = 100;
		}

		if (sz.height == 0) {
			sz.height = 100;
		}

		createCanvas(sz.width, sz.height);

		canevasGauche.addNotifyFlag = true;
		canevasGauche.addNotify();
		canevasDroite.addNotifyFlag = true;

		canevasDroite.addNotify();
	}

	public void ancestorMoved(AncestorEvent event) {
	}

	public void ancestorRemoved(AncestorEvent event) {
		canevasGauche.removeNotify();
		canevasDroite.removeNotify();
	}

	public Canvas3D getOffscreenCanvas3DLeft() {
		if (canevasGauche == null) {
			createCanvas(getWidth(), getHeight());
		}
		return canevasGauche;
	}

	public Canvas3D getOffscreenCanvas3DRight() {
		if (canevasDroite == null) {
			createCanvas(getWidth(), getHeight());
		}
		return canevasDroite;
	}

	public void setBounds(int x, int y, int width, int height) {
		super.setBounds(x, y, width, height);
		createCanvas(width, height);
	}

	private void createCanvas(final int width, final int height) {

		if (getParent() == null) {
			return;
		}

		if (canevasGauche == null) {
			canevasGauche = new InternalCanvas3D(configurationGraphique,
					View.LEFT_EYE_VIEW);
			canevasDroite = new InternalCanvas3D(configurationGraphique,
					View.RIGHT_EYE_VIEW);
		} else {
			if (canevasGauche.getWidth() == width
					&& canevasGauche.getHeight() == height)
				return;
		}
		if (SwingUtilities.isEventDispatchThread()) {
			createOffScreenBuffers(configurationGraphique.getBounds(), width,
					height);
		} else {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					createOffScreenBuffers(configurationGraphique.getBounds(),
							width, height);
				}
			});
		}
	}

	private void createOffScreenBuffers(Rectangle screenRect, int width,
			int height) {
		try {
			accesImage.lock();
			while (estRendu) {

				estImageCopiee = true;
				conditionCopieImage.signal();

				try {
					conditionTailleImage.await();
				} catch (InterruptedException e) {
				}
			}

			canevasGauche.createOffScreenBuffers(configurationGraphique
					.getBounds(), width, height);
			canevasDroite.createOffScreenBuffers(configurationGraphique
					.getBounds(), width, height);

		} finally {
			accesImage.unlock();
		}
	}

	public void paintComponent(Graphics g) {

		super.paintComponent(g);
		if (imageEcran != null && canevasGauche.isRendererRunning()
				&& canevasDroite.isRendererRunning()) {
			g.drawImage(imageEcran, 0, hauteurImage, largeurImage, 0, 0, 0,
					largeurImage, hauteurImage, null);
		}
	}

	protected void processComponentKeyEvent(KeyEvent e) {
		super.processComponentKeyEvent(e);

		Object src = e.getSource();
		e.setSource(canevasGauche);
		canevasGauche.processComponentEvent(e);
		e.setSource(src);
	}

	protected void processFocusEvent(FocusEvent e) {
		super.processFocusEvent(e);

		Object src = e.getSource();
		e.setSource(canevasGauche);
		canevasGauche.processFocusEvent(e);
		e.setSource(src);
	}

	protected void processInputMethodEvent(InputMethodEvent e) {
		super.processInputMethodEvent(e);

		Object src = e.getSource();
		e.setSource(canevasGauche);
		canevasGauche.processInputMethodEvent(e);
		e.setSource(src);
	}

	protected void processKeyEvent(KeyEvent e) {
		super.processKeyEvent(e);

		Object src = e.getSource();
		e.setSource(canevasGauche);
		canevasGauche.processKeyEvent(e);
		e.setSource(src);
	}

	protected void processMouseEvent(MouseEvent e) {
		super.processMouseEvent(e);

		Object src = e.getSource();
		e.setSource(canevasGauche);
		canevasGauche.processMouseEvent(e);
		e.setSource(src);
	}

	protected void processMouseMotionEvent(MouseEvent e) {
		super.processMouseMotionEvent(e);

		Object src = e.getSource();
		e.setSource(canevasGauche);
		canevasGauche.processMouseMotionEvent(e);
		e.setSource(src);
	}

	protected void processMouseWheelEvent(MouseWheelEvent e) {
		super.processMouseWheelEvent(e);

		Object src = e.getSource();
		e.setSource(canevasGauche);
		canevasGauche.processMouseWheelEvent(e);
		e.setSource(src);
	}

	private final class InternalCanvas3D extends Canvas3D implements
			AutoOffScreenCanvas3D {
		private static final long serialVersionUID = 1L;

		private BufferedImage renderImage = null;

		private int eye = -1;

		private boolean addNotifyFlag = false;

		private InternalCanvas3D(GraphicsConfiguration graphicsConfiguration,
				int eye) {
			super(graphicsConfiguration, true);
			this.eye = eye;

			this.setMonoscopicViewPolicy(eye);
		}

		private void createOffScreenBuffers(Rectangle screenRect, int width,
				int height) {
			this.stopRenderer();

			int screenWidth = (int) screenRect.getWidth();
			int screenHeight = (int) screenRect.getHeight();
			Screen3D screen3D = this.getScreen3D();
			screen3D.setSize(screenWidth, screenHeight);
			screen3D.setPhysicalScreenWidth(((double) screenWidth)
					* METRES_PAR_PIXEL);
			screen3D.setPhysicalScreenHeight(((double) screenHeight)
					* METRES_PAR_PIXEL);

			if (renderImage != null)
				renderImage.flush();
			if (eye == View.LEFT_EYE_VIEW && imageEcran != null)
				imageEcran.flush();

			renderImage = new BufferedImage(width, height,
					BufferedImage.TYPE_INT_ARGB);
			ImageComponent2D image = new ImageComponent2D(
					ImageComponent2D.FORMAT_RGBA, renderImage, true, true);
			image.setCapability(ImageComponent2D.ALLOW_IMAGE_READ);
			image.setCapability(ImageComponent2D.ALLOW_IMAGE_WRITE);

			if (eye == View.LEFT_EYE_VIEW) {
				imageEcran = new BufferedImage(width, height,
						BufferedImage.TYPE_INT_ARGB);

				tamponImage = ((DataBufferInt) imageEcran.getRaster()
						.getDataBuffer()).getData();
				tamponGauche = ((DataBufferInt) renderImage.getRaster()
						.getDataBuffer()).getData();

				largeurImage = width;
				hauteurImage = height;
			} else {
				tamponDroite = ((DataBufferInt) renderImage.getRaster()
						.getDataBuffer()).getData();
			}

			this.waitForOffScreenRendering();

			try {
				this.setOffScreenBuffer(image);
			} catch (RestrictedAccessException e) {
				createOffScreenBuffers(screenRect, width, height);
				System.out
						.println("Repeat : createOffScreenBuffer(screenRect, width, height) !!");
			}

			this.startRenderer();
		}

		public void addNotify() {
			if (addNotifyFlag) {
				addNotifyFlag = false;
				super.addNotify();
			} else {
				throw new UnsupportedOperationException("CHANGE ME");
			}
		}

		public Container getParent() {
			return Canvas3DAnaglyph.this.getParent();
		}

		public void postSwap() {
			apresEchange(eye);
		}

		protected void processComponentEvent(ComponentEvent e) {
			super.processComponentEvent(e);
		}

		protected void processFocusEvent(FocusEvent e) {
			super.processFocusEvent(e);
		}

		protected void processInputMethodEvent(InputMethodEvent e) {
			super.processInputMethodEvent(e);
		}

		protected void processKeyEvent(KeyEvent e) {
			super.processKeyEvent(e);
		}

		protected void processMouseEvent(MouseEvent e) {
			super.processMouseEvent(e);
		}

		protected void processMouseMotionEvent(MouseEvent e) {
			super.processMouseMotionEvent(e);
		}

		protected void processMouseWheelEvent(MouseWheelEvent e) {
			super.processMouseWheelEvent(e);
		}
	}
}